package nachos.filesys;

import java.util.ArrayList;

import nachos.filesys.UFileSystem.UnodeManager;
import nachos.machine.Lib;

/**
 * DataManager manages the allocation of data zone and data bitmap zone
 * @author sylvia
 *
 */
public class DataManager {
	
	private static final DataManager instance = new DataManager();
	
	private DataManager(){};
	
	public static DataManager getInstance(){
		return instance;
	}
	
	/**
	 * create a new file directory:
	 * 		1.find a free block, set its bitmap position to "used"
	 * 		2.write contents in the byte array
	 * 		3.return the block number
	 * @param selfUnodeNum
	 * @param parentUnodeNum
	 * @return the new directory's block number
	 */
	public int createDirectory(int selfUnodeNum, int parentUnodeNum){
		// prepare the byte array
		byte[] target = new byte[mBlockSize];
		setDirectoryItem(cSelfIndex, null, selfUnodeNum, target);
		setDirectoryItem(cParentIndex, null, parentUnodeNum, target);
		for (int i = 0; i < 4; i ++){
			setDirectoryItem(i, null, -1, target);
		}
		// find a free position		
		int blockNum = getFreeBlockPos();
		setDataBitmapUsed(blockNum - mDataZoneStartPosition);
		System.out.println("in createDirectory : blockNum = " + blockNum);
		
		//write the byte array into the disk
		diskHelper.writeBlock(blockNum, 1, target);
				
		return blockNum;
	}

	public void createRootDirectory(){
		// prepare the byte array
		byte[] target = new byte[mBlockSize];
		setDirectoryItem(cSelfIndex, null, 0, target);
		setDirectoryItem(cParentIndex, null, 0, target);
		for (int i = 0; i < 4; i ++){
			setDirectoryItem(i, null, -1, target);
		}		
		
		//write!
		diskHelper.writeBlock(mDataZoneStartPosition, 1, target);
		
		//for debug....
		System.out.println("debug : ");
		for (DirectoryItem d : this.getDirectoryItemsByBlockNum(mDataZoneStartPosition)){
			System.out.println("\tname = " + d.name + "  unode number = " + d.unodeNum);
		}
	}

	public void setDataBitmapUsed(int blockNum){
		setDataBitmap(blockNum, true);
	}
	
	public void setDataBitmapFree(int blockNum){
		this.setDataBitmap(blockNum, false);
	}
	
	/**
	 * set value to the specipic position in data bitmap zone 
	 * @param blockNum the relavant number of the block
	 * @param value true for used, and false for free
	 */
	private void setDataBitmap(int blockNum, boolean value){
		// calculate the block number in the bitmap zone
		int targetBlockNumber = (blockNum / 8) / mBlockSize;
		int targetByteNumber = (blockNum / 8) % mBlockSize;
		int targetBitNumber = blockNum % 8;
		
		// read in the target block
		byte[] temp = new byte[mBlockSize]; 
		diskHelper.readBlock(targetBlockNumber, 1, temp);
		
		// set the target position
		if (value){
			byte b = (byte) (1 << (7-targetBitNumber));
			temp[targetByteNumber] = (byte) (temp[targetByteNumber] | b);
		}
		else{
			byte b = (byte) (0xff ^ (byte) (1 << (7-targetBitNumber)));
			temp[targetByteNumber] = (byte) (temp[targetByteNumber] & b);
		}
		
		// write back the target block
		diskHelper.writeBlock(targetBlockNumber, 1, temp);
	}
	
	/**
	 * Search the data bitmap zone to get a free block position.
	 * @return free block position, absolute
	 */
	private int getFreeBlockPos(){
		int blockPos = -1, bytePos = -1;
		byte t = (byte)0xff;
		boolean found = false;
		for (int i = 0; i < mDataBitmapZoneBlockNumber; i ++){
			int blockOffset = mDataBitmapZoneStartPosition + i;
			byte[] temp = new byte[mBlockSize];
			diskHelper.readBlock(blockOffset, 1, temp);
			for (int j = 0; j < temp.length; j ++){
				//FREE!!
				if (temp[j] != 0xff){
					found = true;
					blockPos = i;
					bytePos = j;
					t = temp[i];
					break;
				}
			}
			if (found)
				break; 
		}
		Lib.assertTrue(found, "no free position!");
		int blockNum = blockPos * mBlockSize * 8 + bytePos * 8 + getFreeBitPos(t);
		int result = blockNum + mDataZoneStartPosition;
		System.out.println("get free block #" + result);
		return result;
	}
	
	public ArrayList<Integer> requestFreeBlock(int n){
		ArrayList<Integer> al = new ArrayList<Integer>();
		for (int i = 0; i < n; i ++){
			int t = this.getFreeBlockPos();
			this.setDataBitmapUsed(t);
			al.add(t);
		}
		return al;
	}
	
	/**
	 * get free bit pos in a byte, this byte t must has a free bit. 
	 * @param t
	 * @return the first fee bit in t, started from left side.
	 */
	private int getFreeBitPos(byte t){
		Lib.assertTrue(t != 0xff);
		for (int i = 0; i < 8; i ++){
			if (((1 << (7-i)) & t) == 0x00)
				return i;
		}
		Lib.assertNotReached();
		return -1;
	}
	
	/**
	 * remove an item of directory by name in a byte array
	 * @param target
	 * @param name
	 * @return return the index number if find and remove, or -1 if not found 
	 */
	private int removeDirectoryItemFromByte(byte[] target, String name){
		String _name;
		for (int i = 0; i < 4; i ++){
			_name = getDirectoryItemNameFromByte(target, i);
			if (name.equals(_name)){
				setDirectoryItem(i, null, -1, target);
				return i;
			}			
		}
		return -1;
	}
	
	/**
	 * 
	 * @param targetUnodeNum the parent unode numebr
	 * @param itemName the new item's name to add
	 * @param addUnodeNum the new unode number to add
	 */
	public void addDirectoryItem(int dirUnodeNum, String itemName, int addUnodeNum){
		// read in the target blocks, until find a free position
		Unode dirUnode = UnodeManager.getUnode(dirUnodeNum);
		dirUnode.printInfo("dirUnode info:");
		int directoryBlockCount = dirUnode.getFileSize() / mBlockSize + 1;
		boolean found = false;
		int targetIndexInBlock = -1;
		byte[] temp = new byte[mBlockSize];
		int phyBlockNum = -1;
		int targetBlockNum = -1;
		int targetItemIndex = -1;
		for (int i = 0; i < directoryBlockCount; i ++){
			phyBlockNum = dirUnode.searchBlockInIndexTable(i);
			dirUnode.printInfo("in addDirevtoryItem : dirUnode info");
			System.out.println("in addDirevtoryItem : phyBlockNum = " + phyBlockNum);
			// read a block in
			diskHelper.readBlock(phyBlockNum, 1, temp);
			for (int j = 0; j < 4; j ++){
				// find a free position!!
				if (getDirectoryItemUnodeNumFromByte(temp, j) == -1){
					found = true;
					targetItemIndex = j;
					targetBlockNum = phyBlockNum;
					break;
				}
			}
			if (found)
				break;			
		}
		Lib.assertTrue(found, "cannot find free item to write!");
		
		// set the item
		this.setDirectoryItem(targetItemIndex, itemName, addUnodeNum, temp);
		
		// write back the target block
		diskHelper.writeBlock(targetBlockNum, 1, temp);
		
	}

	/**
	 * remove a directory item according the Unode numebr
	 * @param unodeNum the directory unode number
	 * @param name name of the file to delete
	 */
	public void removeDirectoryItemByUnodeNum(int dirUnodeNum, String name){
		// read in the target block, search the "name" file
		Unode directoryUnode = UnodeManager.getUnode(dirUnodeNum);
		int directoryBlockCount = directoryUnode.getFileSize() / mBlockSize + 1;
		boolean found = false;
		int targetIndexInBlock = -1;
		byte[] temp = new byte[mBlockSize];
		int phyBlockNum = -1;
		for (int i = 0; i < directoryBlockCount; i ++){
			phyBlockNum = directoryUnode.searchBlockInIndexTable(i);
			directoryUnode.printInfo("in removeDirevtoryItemByUnodeNum : directoryUnode info");
			System.out.println("in removeDirevtoryItemByUnodeNum : phyBlockNum = " + phyBlockNum);
			// read a block in
			diskHelper.readBlock(phyBlockNum, 1, temp);
			targetIndexInBlock = this.removeDirectoryItemFromByte(temp, name); 
			if (targetIndexInBlock != -1)
				found = true;
				break;
		}
		Lib.assertTrue(found, "try to remove a non-exists item from directory!");
		
		// fill the deleted position with the last item
		int lastIndexInBlock = directoryUnode.getFileSize() % mBlockSize / mNameLength - 1;
		int lastPhyBlockNum = directoryUnode.searchBlockInIndexTable(directoryBlockCount-1);
		byte[] lastTemp = new byte[mBlockSize];
		diskHelper.readBlock(lastPhyBlockNum, 1, lastTemp);
		String lastName = this.getDirectoryItemNameFromByte(lastTemp, lastIndexInBlock);
		int lastUnodeNum = this.getDirectoryItemUnodeNumFromByte(lastTemp, lastIndexInBlock);
		this.setDirectoryItem(targetIndexInBlock, lastName, lastUnodeNum, temp);
		diskHelper.writeBlock(phyBlockNum, 1, temp);
		
		// set the last item empty
		this.setDirectoryItem(lastIndexInBlock, null, -1, lastTemp);
		diskHelper.writeBlock(lastPhyBlockNum, 1, lastTemp);
		
		// file size -2
		directoryUnode.increaseFileSize(-254);
		UnodeManager.writeUnode(dirUnodeNum, directoryUnode);
	}
	
	private void setDirectoryItem(int directoryIndexCode, String name, int unodeNum, byte[] target){
		byte[] tempInt = Lib.bytesFromInt(unodeNum);
		byte[] tempName = new byte[mNameLength];
		switch (directoryIndexCode){
		case cSelfIndex:
			System.arraycopy(tempInt, 0, target, mSelfUnodeNumPosition, 4);
			break;
		case cParentIndex:
			System.arraycopy(tempInt, 0, target, mParentUnodeNumPosition, 4);
			break;
		case 0:
		case 1:
		case 2:
		case 3:
			System.arraycopy(tempInt, 0, target, mSonUnodePosition[directoryIndexCode], 4);
			if (name != null){
				byte[] t = name.getBytes();
				System.arraycopy(t, 0, tempName, 0, t.length);
			}
			System.arraycopy(tempName, 0, target, mSonNamePosition[directoryIndexCode], mNameLength);
			break;
		default:
			Lib.assertNotReached();				
		}
		return;
	}
	
	/**
	 * get unode number from byte array "target" and directory index code
	 * @param target
	 * @param directoryIndexCode
	 * @return
	 */
	public int getDirectoryItemUnodeNumFromByte(byte[] target, int directoryIndexCode){
		int result = -1;
		switch(directoryIndexCode){
		case cSelfIndex:
			return Lib.bytesToInt(target, mSelfUnodeNumPosition, 4);
		case cParentIndex:
			return Lib.bytesToInt(target, mParentUnodeNumPosition, 4);
		case 0:
		case 1:
		case 2:
		case 3:
			byte[] temp = new byte[4];
			System.arraycopy(target, mSonUnodePosition[directoryIndexCode], temp, 0, 4);
			result = Lib.bytesToInt(temp, 0, 4); 
			break;
		default:
			Lib.assertNotReached();
		}
		return result;
	}
	
	/**
	 * get item name from byte array and index code
	 * @param target
	 * @param directoryIndexCode
	 * @return
	 */
	public String getDirectoryItemNameFromByte(byte[] target, int directoryIndexCode){
		String name = null;
		switch(directoryIndexCode){
		case cSelfIndex:
			return ".";
		case cParentIndex:
			return "..";
		case 0:
		case 1:
		case 2:
		case 3: 
			name = Lib.bytesToString(target, mSonNamePosition[directoryIndexCode], mNameLength);
			break;
		default:
			Lib.assertNotReached();
		}
		return name;
	}
	
	/**
	 * get all items' names and unode numbers by block number.
	 * @param dirBlockNum
	 * @return
	 */
	public ArrayList<DirectoryItem> getDirectoryItemsByBlockNum(int dirBlockNum){
		ArrayList<DirectoryItem> result = new ArrayList<DirectoryItem>();
		byte[] tempBlock = new byte[mBlockSize];
		diskHelper.readBlock(dirBlockNum, 1, tempBlock);
		for (int i = 0; i < 6; i ++){
			result.add(new DirectoryItem(getDirectoryItemNameFromByte(tempBlock, i),
					getDirectoryItemUnodeNumFromByte(tempBlock, i)));
		}	
		return result;
	}
	
	public void printDirectoryInfoByUnodeNum(int unodeNum){
		System.out.println("print directory (unode#" + unodeNum + ")'s items:");
		Unode unode = UnodeManager.getUnode(unodeNum);
		int blockCount = unode.getFileSize() / mBlockSize + 1;
		for (int i = 0; i < blockCount; i ++){
			int phyBlockNum = unode.searchBlockInIndexTable(i);
			ArrayList<DirectoryItem> al = this.getDirectoryItemsByBlockNum(phyBlockNum);
			for (DirectoryItem e : al){
				System.out.println("\tname = " + e.name + " unode number = " + e.unodeNum);
			}
		}
	}
	
	public ArrayList<DirectoryItem> getDirectoryItemByUnodeNum(int unodeNum){
		ArrayList<DirectoryItem> result = new ArrayList<DirectoryItem>();
		Unode unode = UnodeManager.getUnode(unodeNum);
		int blockCount = unode.getFileSize() / mBlockSize + 1;
		for (int i = 0; i < blockCount; i ++){
			int phyBlockNum = unode.searchBlockInIndexTable(i);
			ArrayList<DirectoryItem> al = this.getDirectoryItemsByBlockNum(phyBlockNum);
			result.addAll(al);
		}		
		return result;
	}
	
	public static final int
		cSelfIndex = 4,
		cParentIndex = 5;
	
	private final static int mBlockSize = DiskHelper.getBlockSize();	
	private final static int mSelfUnodeNumPosition = 0;
	private final static int mParentUnodeNumPosition =4;
	private final static int[] mSonNamePosition = {8, 262, 516, 770};
	private final static int[] mSonUnodePosition = {258, 512, 766, 1020};
	private final static int mNameLength = 250;
	
	private final static int mDataZoneStartPosition = UFileSystem.sDataZoneStartPosition;
	private final static int mDataBitmapZoneStartPosition = UFileSystem.sDataBitmapZoneStartPosition;
	private final static int mDataZoneBlockNumber = UFileSystem.sDataZoneBlockNumber;
	private final static int mDataBitmapZoneBlockNumber = UFileSystem.sDataBitmapZoneBlockNumber;
	
	private final static DiskHelper diskHelper = DiskHelper.getInstance();
	
	/**
	 * DirectoryItem class, store <name, unodeNum> pair
	 * @author sylvia
	 *
	 */
	class DirectoryItem{
		DirectoryItem(String _name, int _unodeNum){
			name = _name;
			unodeNum = _unodeNum;
		}
		String name = null;
		int unodeNum = -1;
	}	
}
